---
title: Custom tab layouts
description: Learn how to use headless tab components to create custom tab layouts in Expo Router.
sidebar_title: Custom tabs
---

import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon';

import { BoxLink } from '~/ui/components/BoxLink';
import { Collapsible } from '~/ui/components/Collapsible';
import { FileTree } from '~/ui/components/FileTree';

> **important** Experimentally available in SDK 52 and later.

Expo Router offers a set of components to create custom tab layouts via the submodule [`expo-router/ui`](/versions/latest/sdk/router-ui/). Unlike the React Navigation styled `Tabs`, these components are unstyled and flexible. They are designed to allow you build complex UI patterns from scratch in your project.

For other tab layouts see:

<BoxLink
  title="Native tabs"
  href="/router/advanced/native-tabs/"
  description="See native tabs if you want to achieve a native look and feel for your tab bar."
  Icon={BookOpen02Icon}
/>

<BoxLink
  title="JavaScript tabs"
  href="/router/advanced/tabs/"
  description="See JavaScript tabs if you already use React Navigation's tabs."
  Icon={BookOpen02Icon}
/>

## Anatomy of custom Tabs components

There are four components offered by `expo-router/ui` to create custom tab layouts:

| Component    | Description                                                                                                                     |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `Tabs`       | Wrapper component which contains the `<View>` for the tabs.                                                                     |
| `TabList`    | The containing `<View>` for the list of `TabTrigger` components.                                                                |
| `TabTrigger` | A trigger component to switch to the specified tab. It is used to define the route using `href` prop and a `name` for each tab. |
| `TabSlot`    | A slot to render the currently selected tab.                                                                                    |

A bare minimum structure of a custom tab layout would consist of a `TabList` (containing `TabTrigger` components for each tab) and a`TabSlot`, all within the `Tabs` component, as shown here:

```tsx app/(tabs)/_layout.tsx|collapseHeight=440
import { Tabs, TabList, TabTrigger, TabSlot } from 'expo-router/ui';
import { Text } from 'react-native';

// Defining the layout of the custom tab navigator
export default function Layout() {
  return (
    <Tabs>
      <TabSlot />
      <TabList>
        <TabTrigger name="home" href="/">
          <Text>Home</Text>
        </TabTrigger>
        <TabTrigger name="article" href="/article">
          <Text>Article</Text>
        </TabTrigger>
      </TabList>
    </Tabs>
  );
}
```

## Creating routes

The `TabList` contains all the routes available within the tab navigator. It must be an immediate child of `Tabs`. Each route is defined by a `TabTrigger` within the `TabList`. A `TabTrigger` within a `TabList` must include a `name` and a `href` prop.

Typically, the `TabList` defines both the available tab routes and the appearance of the tabs, with the children of each `TabTrigger` defining the appearance of each tab button.

> **Note:** A `name` can be any `string`. This is a user-defined name for the Tab.

### Dynamic routes

Dynamic routes are allowed and can be provided with values via the `href`.

<FileTree files={['_layout.tsx', ['[slug].tsx']]} />

The trigger `<TabTrigger name="dynamic page" href="/hello-world" />` will create a tab for **[slug].tsx** with the params `{ slug: 'hello-world' }`. This setup can be useful for displaying an arbitrary number of tabs in the tab bar, based on end-user data, such as showing a separate tab for each user profile in an app.

### Ambiguous routes

<FileTree files={['_layout.tsx', ['(one,two)/route.tsx', 'A route within a shared group']]} />

The `href` values provided to `TabTrigger` must always point to a single route. In the above example of a shared route, href `/route` is not allowed, as it could refer to either `/(one)/route` or `/(two)/route`. However, specifying the route group within the href would work (for example,`href="/(one)/route"`).

### Nested routes

<FileTree
  files={[
    ['_layout.tsx'],
    ['(stack-one)/_layout.tsx', 'A <Stack> layout'],
    ['(stack-one)/(stack-two)/_layout.tsx', 'Nested <Stack> layout'],
    '(stack-one)/(stack-two)/route.tsx',
  ]}
/>

A `TabTrigger` can link to a deeply nested route. `<TabTrigger name="route" href="/route" />` will show the **(stack-one)/(stack-two)/route.tsx** route. This tab will be controlled by that route's parent navigator (that is, the navigator within **stack-two_layout.tsx**). This navigation is similar to a deep link.

## Rendering routes

The `TabSlot` component renders the current route. `TabSlot` can be nested inside other components within `Tabs` but cannot be within the `TabList`.

```tsx app/_layout.tsx
<Tabs>
  <TabList>
    <TabTrigger name="home" href="/">
      <Text>Home</Text>
    </TabTrigger>
  </TabList>
  {/* Customize how `<TabSlot />` is rendered. */}
  /* @info <CODE>TabSlot</CODE> can be nested inside custom components. */
  <View>
    <View>
      <TabSlot />
    </View>
  </View>
  /* @end */
</Tabs>
```

## Switching tabs

Tabs can be switched via a `Link` or using the imperative APIs. However, these APIs will always perform a navigation action (they will switch tabs and might change the URL). To switch tabs without performing any navigation, you should use a `TabTrigger`. A `TabTrigger` is an unstyled `<View>` that will switch tabs when pressed, much like how text and components can be wrapped in `Link` to make them pressable navigation elements.

### Resetting navigation

The `reset` prop from `TabTrigger` can be used to control when a tab resets its navigation state. The options are `always`, `onLongPress` and `never`. This is particularly useful for a stack navigator nested inside a tab. For example, `<TabTrigger name="home" reset="always" />` will return the user to the index route inside a tab's nested stack navigator.

## TabTrigger

The `TabTrigger` is used to switch tabs, but also has a dual role of defining what routes are available as a tab.

### Within TabList

When a `TabTrigger` is used as a child of `TabList`, that defines what routes are available within the tab navigator. These `TabTrigger` need to include both the `name` and `href` props, as they define the URL for that tab and a custom name that can be used to refer to the tab. If the `TabTrigger` components also contain text or other components as children, then those will also render as the tab buttons. However, you can define the `TabTrigger`'s within the `TabList` without any UI, and they can then be invoked by `TabTrigger`'s outside of the `TabList`.

### Outside TabList

An additional `TabTrigger` can be defined outside of a `TabList`, allowing you to perform the same action as the `TabTrigger` that is defined in the `TabList`. In this case, the `TabTrigger` will not have an `href` prop. Rather, it will perform the same action as the primary `TabTrigger` with the same `name` prop. This allows you to create components that can switch tabs and be agnostic to your current navigation state. Note that all `TabTrigger`'s need to at least be descendants of the `Tabs` component, or else they will be considered to be outside the tab navigator and unable to invoke it.

## Customizing appearance

All components are rendered unstyled as a `<View>`, except `TabTrigger` which renders as a `<Pressable>`. This allows you to provide a custom `style` prop to customize their appearance. Styling `TabList` is similar to customizing the tab bar in React Navigation, while styling `TabTrigger` affects the appearance of tab buttons.

If you need to change the structure of a component, you can override its underlying component by using the `asChild` props. The component then acts as a slot, and will forward its props to its immediate child.

```tsx Custom TabList
<Tabs>
  <TabSlot />
  /* @info Change the appearance of the <CODE>TabList</CODE>. */
  <TabList asChild>
    {/* Render a custom TabList */}
    <CustomTabList>
      <TabTrigger name="home" href="/">
        <Text>Home</Text>
      </TabTrigger>
    </CustomTabList>
  </TabList>
  /* @end */
</Tabs>
```

```tsx Custom Button
<Tabs>
  <TabSlot />
  <TabList asChild>
    /* @info Change the appearance of the <CODE>TabTrigger</CODE>. */
    <TabTrigger name="home" href="/" asChild>
      {/* Render a custom button */}
      <CustomButton>
        <Text>Home</Text>
      </CustomButton>
    </TabTrigger>
    /* @end */
  </TabList>
</Tabs>
```

### Multiple tab bars

The `TabList` is both the configuration and default appearance of the `Tabs`, but it is not the only way to render a tab bar. By hiding the `TabList`, you can construct custom tab bars using `TabTrigger`.

{/* prettier-ignore */}
```tsx Multiple tab bars example
<Tabs>
  <TabSlot />
  {/* A custom tab bar */}
  <View>
    /* @info A custom tab bar. <CODE>TabTrigger</CODE>'s outside of TabList do not need the <CODE>href</CODE> prop. */
    <View>
      <TabTrigger name="home">
        <Text>Home</Text>
      </TabTrigger>
      <TabTrigger name="article">
        <Text>article</Text>
      </TabTrigger>
    </View>
    /* @end */
  </View>
  /* @info <CODE>TabList</CODE> needs to be rendered, but not necessary displayed. */
  <TabList style={{ display: 'none' }}>
    /* @end */
    <TabTrigger name="home" href="/">
      <Text>Home</Text>
    </TabTrigger>
    <TabTrigger name="article" href="/article">
      <Text>article</Text>
    </TabTrigger>
  </TabList>
</Tabs>
```

`TabTrigger` will forward an `isFocused` prop, so you can create a separate tab button component that reacts to focused status.

```tsx TabButton.tsx
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { TabTriggerSlotProps } from 'expo-router/ui';
import { ComponentProps, Ref } from 'react';
import { Text, Pressable, View } from 'react-native';

type Icon = ComponentProps<typeof FontAwesome>['name'];

export type TabButtonProps = TabTriggerSlotProps & {
  icon?: Icon;
  ref: Ref<View>;
};

export function TabButton({ icon, children, isFocused, ...props }: TabButtonProps) {
  return (
    <Pressable
      {...props}
      style={[
        {
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          flexDirection: 'column',
          gap: 5,
          padding: 10,
        },
        isFocused ? { backgroundColor: 'white' } : undefined,
      ]}>
      <FontAwesome name={icon} />
      <Text style={[{ fontSize: 16 }, isFocused ? { color: 'white' } : undefined]}>{children}</Text>
    </Pressable>
  );
}
```

<Collapsible summary="Expo SDK 52 / React 18 and earlier">

In Expo SDK 52 and earlier (React 18), use the legacy `forwardRef` function to access the `ref` handle.

```diff
- import { ComponentProps, Ref } from 'react';
+ import { ComponentProps, Ref, forwardRef } from 'react';

- export function TabButton({ ref }) {
+ export const TabButton = forwardRef((props: TabButtonProps, ref: Ref<View>) => {
```

</Collapsible>

### Hooks

All components also have a hook version giving you control over the render tree. See the [Router UI Reference](/versions/latest/sdk/router-ui/) for a full list of the hooks available.

Using hooks is considered advanced usage of this library. For most use-cases, using the components with `asChild` should give you enough control over the render tree.

If you are developing a custom `<TabTrigger />`, you may also need to develop a custom `<TabList />` as `<TabList />` uses the [`useTabsWithChildren()`](/versions/latest/sdk/router-ui/#usetabswithchildrenoptions) which requires using the exported `<TabTrigger />` component.

### Customizing how tab screens are rendered

The `TabSlot` accepts a `renderFn` property. This function can be used to override how your screen is rendered, allowing you to implement advanced functionality such as animations or persisting/unmounting screens. See the [Router UI Reference](/versions/latest/sdk/router-ui/) for more information.

## Common questions

<Collapsible summary="How do I create multiple tabs for the same route?">

<FileTree
  files={[
    ['_layout.tsx', 'Tabs layout'],
    ['(movie,tv)/[id].tsx', ''],
  ]}
/>

You should add the route to a shared group and create a separate `TabTrigger` for each group `group`.

</Collapsible>

<Collapsible summary="How do I hide a tab?">

Not rendering the `TabTrigger` will remove that tab (and its navigation state) from your app.

</Collapsible>

<Collapsible summary="How do I create animated tabs?">

You can provide a custom renderer to `TabSlot` to customize how it renders a screen. You can use this to detect when screen is focused an animate appropriately.

</Collapsible>

<Collapsible summary="Can I use relative hrefs?">

<FileTree
  files={[
    ['directory/_layout.tsx', 'The local pathname is /directory'],
    ['directory/page.tsx', 'The pathname is /directory/page'],
    ['directory/profile.tsx', 'The pathname is /directory/profile'],
  ]}
/>

A `TabTrigger` with a relative href is relative to the local path name `Tabs` was rendered on. This is different from normal relative hrefs which are relative to the current displayed route. For example, the `<TabTrigger href="./profile" />` will resolve to `/directory/profile`, even when the `/directory/page` route is showing. Expo recommends against using relative hrefs.

</Collapsible>
